package org.theqcp.wow.model.player;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Set;

import org.theqcp.wow.model.exceptions.PlayerRaceNotAllowedException;
import org.theqcp.wow.model.exceptions.TalentNotFoundException;
import org.theqcp.wow.model.items.Equipment;
import org.theqcp.wow.model.mechanics.Attribute;
import org.theqcp.wow.model.mechanics.ClassType;
import org.theqcp.wow.model.mechanics.Race;
import org.theqcp.wow.model.mechanics.Slot;

public abstract class Player {

    /**
     * "White" stats retrieved from a datasource
     */
    private HashMap<Attribute, Double> baseStats = new HashMap<Attribute, Double>();

    /**
     * "Green" stats calculated through equipment and tallents
     */
    private HashMap<Attribute, Double> effectiveStats = new HashMap<Attribute, Double>();

    /**
     * Stores the list of equipment that has been equiped on the player
     */
    private HashMap<Slot, Equipment> equipmentTable = new HashMap<Slot, Equipment>();

    /**
     * Stores the talents available to the player
     */
    private ArrayList<Talent> talents = new ArrayList<Talent>();

    /**
     * The trade skills of the player
     * 
     */
    private HashMap<String, Integer> tradeskills = new HashMap<String, Integer>();

    /**
     * The name of the player
     */
    private String name;

    /**
     * The realm the player came from
     */
    private String realm;

    /**
     * The character's level.
     */
    private int level;

    /**
     * Any sort of data-source id from the data layer
     */
    private String id;

    /**
     * Race of the character
     */
    private Race r;

    /**
     * Class of the character
     */
    private ClassType c;

    /**
     * Get the name of the player.
     * 
     * @return
     */
    public String getName() {
	return name;
    }

    /**
     * Set the name of the player.
     * 
     * @param name
     */
    public void setName( String name ) {
	this.name = name;
    }

    /**
     * Get the realm name that the player came from
     * 
     * @return
     */
    public String getRealm() {
	return realm;
    }

    /**
     * Set the realm name of the player
     * 
     * @param realm
     */
    public void setRealm( String realm ) {
	this.realm = realm;
    }

    /**
     * Get the id for the player that was set by the data service.
     * 
     * @return
     */
    public String getId() {
	return id;
    }

    /**
     * Set an id to identifiy the player.
     * 
     * @param id
     */
    public void setId( String id ) {
	this.id = id;
    }

    /**
     * Set the value of a base stat
     * 
     * @param stat
     * @param value
     */
    public void setBaseStat( Attribute stat, Double value ) {

	if( value == null || value.equals(0.0) )
	    return;

	if( this.baseStats.containsKey(stat) ) {
	    this.baseStats.remove(stat);
	}

	// put in table
	this.baseStats.put(stat, value);

    }

    /**
     * Get the value of a base stat
     * 
     * @param stat
     */
    public Double getBaseStat( Attribute stat ) {
	Double statValue = new Double(0.0);

	if( this.baseStats.containsKey(stat) ) {
	    statValue = this.baseStats.get(stat);
	}

	return statValue;
    }

    /**
     * Used to change a stat in the effective table. Uses the baseStats from the
     * table as a baseline. This is used by recalulation mechansim to calculate
     * values from talents and gear.
     * 
     * @param stat
     * @param valueToIncrement
     * @return
     */
    public Double incrementEffectiveStat( Attribute stat, Double deltaValue ) {

	// 0.0 and null have no effect on the effective stats table
	if( deltaValue == null || deltaValue.equals(0.0) )
	    return new Double(0.0);

	// get the base stat, 0 if not entered
	Double base = this.getEffectiveStat(stat);

	// calculate effective
	Double effective = new Double(base.doubleValue() + deltaValue.doubleValue());

	if( this.effectiveStats.containsKey(stat) ) {
	    this.effectiveStats.remove(stat);
	}

	// put in table
	this.effectiveStats.put(stat, effective);

	// return the new effective stat
	return effective;
    }

    /**
     * Used by the recalculation mechanism to force the setting of a new stat,
     * designed to be used for more complicated mafs.
     * 
     * @param stat
     * @param newValue
     * @return
     */
    public Double setEffectiveStat( Attribute stat, Double newValue ) {

	// 0.0 and null have no effect on the effective stats table
	if( newValue == null || newValue.equals(0.0) ) {
	    if( this.effectiveStats.containsKey(stat) ) {
		this.effectiveStats.remove(stat);
	    }
	    return new Double(0.0);
	}

	if( this.effectiveStats.containsKey(stat) ) {
	    this.effectiveStats.remove(stat);
	}

	// put in table
	this.effectiveStats.put(stat, newValue);

	// return the new effective stat
	return newValue;
    }

    /**
     * Gets the calculated value from the effective stats table. If the stat has
     * not been calculated it will fall back and grab the base stat from the
     * base stat table.
     * 
     * @param stat
     * @return
     */
    public Double getEffectiveStat( Attribute stat ) {
	Double statValue = new Double(0.0);

	if( this.effectiveStats.containsKey(stat) ) {
	    statValue = this.effectiveStats.get(stat);
	}

	if( statValue == null || statValue == 0.0 ) {
	    statValue = this.getBaseStat(stat);
	}

	return statValue;
    }

    /**
     * Get the difference between the effective stat and the base stat of the
     * given type. If there is no difference or if the effective is less then a
     * value of 0.0 is returned.
     * 
     * @param stat
     * @return
     */
    public Double getStatDelta( Attribute stat ) {

	Double base = this.getBaseStat(stat);
	Double eff = this.getEffectiveStat(stat);

	Double val = base;

	if( eff > val ) {
	    val = eff - val;
	} else {
	    val = 0.0;
	}

	return val;
    }

    /**
     * Clear a stat from the stat table.
     * 
     * @param stat
     */
    protected void clearEffectiveStat( Attribute stat ) {

	if( this.effectiveStats.containsKey(stat) ) {
	    this.effectiveStats.remove(stat);
	}

    }

    /**
     * Clear all effective stats as calculated, and return to the uncalculated
     * state.
     * 
     */
    private void clearEffectiveStats() {
	this.effectiveStats.clear();
    }

    /**
     * Equip an item, if the item can go in more than one slot then it will pick
     * the first empty slot. If there is no empty slot it will pick the lower
     * numbered slot, removing any equipment in that slot.
     * 
     * @param item
     */
    public void equipItem( Equipment item ) {

	// return if something is not right
	if( item == null ) {
	    return;
	}

	// get slot
	Slot slot = item.getEquipSlot();

	// get requires and companion
	Slot[] companion = slot.getCompanionSlots();
	Slot[] requires = slot.getRequiresAlso();

	// check companion slots for empty
	for( Slot c: companion ) {
	    if( !this.equipmentTable.containsKey(c) ) {
		slot = c;
		break;
	    }
	}

	// remove equipment from required slots
	for( Slot r: requires ) {
	    this.equipmentTable.remove(r);
	}

	// base slot
	Slot base = slot.getBaseSlot();

	// if anything requires this base slot, remove it
	for( Slot s: this.equipmentTable.keySet() ) {
	    if( Arrays.asList(s.getRequiresAlso()).contains(base) ) {
		this.equipmentTable.remove(s);
	    }
	}

	// make sure to unequip base slot
	this.equipmentTable.remove(base);

	// add new equipment
	this.equipmentTable.put(slot, item);

	// recaluclate items
	this.recalc();

    }

    /**
     * Equip an item in the specified slot. If an item is alread in that slot,
     * then remove that item and equip the new one. Slot is specified by the
     * string that represents the slot number.
     * 
     * @param slot
     * @param item
     */
    public void equipItem( Slot slot, Equipment item ) {

	// return if something is not right
	if( slot == null || item == null ) {
	    return;
	}

	// remove equipment from table, if it exists
	if( this.equipmentTable.containsKey(slot) ) {
	    this.equipmentTable.remove(slot);
	}

	// get requires and remove equipment from required slots
	Slot[] requires = slot.getRequiresAlso();
	for( Slot r: requires ) {
	    this.equipmentTable.remove(r);
	}

	// base slot
	Slot base = slot.getBaseSlot();

	// if anything requires this base slot, remove it
	for( Slot s: this.equipmentTable.keySet() ) {
	    if( Arrays.asList(s.getRequiresAlso()).contains(base) ) {
		this.equipmentTable.remove(s);
	    }
	}

	// make sure to unequip base slot
	this.equipmentTable.remove(base);

	// add new equipment
	this.equipmentTable.put(slot, item);

	// recaluclate items
	this.recalc();
    }

    /**
     * Remove the item from the given slot. If the slot doesn't have an item in
     * it then there is no effect.
     * 
     * @param slot
     */
    public void unequipItem( Slot slot ) {

	// return if something is not right
	if( slot == null ) {
	    return;
	}

	// remove item from slot/table
	this.equipmentTable.remove(slot);

	// recalculate effective stats
	this.recalc();
    }

    /**
     * Get a count of equiped items.
     * 
     * @return
     */
    public int getEquipedCount() {
	return this.equipmentTable.size();
    }

    /**
     * Calculate the effective stats from equipment. This should be done before
     * the application of talents. <br/>
     * <br/>
     * Preconditions:<br/>
     * <ul>
     * <li>The equipment list has items in it.</li>
     * </ul>
     * Postconditions:<br/>
     * <ul>
     * <li>The old values are cleared from the effective stats.</li>
     * <li>The effective stats list now has the stats from equipment in it.</li>
     * </ul>
     */
    protected void calculateEffectiveStatsFromEquipment() {

	// clear
	this.clearEffectiveStats();

	// cycle through equipment (no order)
	Collection<Equipment> eqList = this.equipmentTable.values();

	// go through the equipment
	for( Equipment eq: eqList ) {

	    // equip! and go through each stat
	    for( Attribute stat: eq.getAvailableStats() ) {

		// get the value of the stat
		Double value = eq.getStat(stat);

		// increment effective
		this.incrementEffectiveStat(stat, value);
	    }
	}

	// all effective stats should be set up
    }

    /**
     * Get the player's race
     * 
     * @return
     */
    public Race getRace() {
	return r;
    }

    /**
     * Set the player's race
     * 
     * @param r - Race for player
     * @throws PlayerRaceNotAllowedException thrown when race specified is not
     *         valid for the player class
     */
    public void setRace( Race r ) throws PlayerRaceNotAllowedException {
	this.r = r;
    }

    /**
     * Get the class type of the player
     * 
     * @return
     */
    public ClassType getClassType() {
	return c;
    }

    /**
     * Set the player's class type
     * 
     * @param c
     */
    public void setClassType( ClassType c ) {
	this.c = c;
    }

    /**
     * Get the player's level
     * 
     * @return
     */
    public int getLevel() {
	return level;
    }

    /**
     * Set the player's level
     * 
     * @param level
     */
    public void setLevel( int level ) {
	this.level = level;
    }

    /**
     * Return a string representation of the player
     */
    public String toString() {
	// normal
	String output = String.format("<%s> - <%s>\n", this.getName(), this.getRealm());

	// stats (output in the same order as wow armory)
	output += String.format("STR: %s (%s)\n", this.getEffectiveStat(Attribute.STRENGTH),
		this.getBaseStat(Attribute.STRENGTH));
	output += String.format("AGI: %s (%s)\n", this.getEffectiveStat(Attribute.AGILITY),
		this.getBaseStat(Attribute.AGILITY));
	output += String.format("STA: %s (%s)\n", this.getEffectiveStat(Attribute.STAMINIA),
		this.getBaseStat(Attribute.STAMINIA));
	output += String.format("INT: %s (%s)\n", this.getEffectiveStat(Attribute.INTELLECT),
		this.getBaseStat(Attribute.INTELLECT));
	output += String.format("SPI: %s (%s)\n", this.getEffectiveStat(Attribute.SPIRIT),
		this.getBaseStat(Attribute.SPIRIT));
	output += String.format("AP: %s (%s)\n", this.getEffectiveStat(Attribute.ATTACKPOWER),
		this.getBaseStat(Attribute.ATTACKPOWER));

	// secondary calculated stats
	output += String.format("CRIT: %s (%s)\n", this.getEffectiveStat(Attribute.CRITPERCENT),
		this.getEffectiveStat(Attribute.CRITRATING));
	output += String.format("EXPERTISE: %s (%s)\n", this.getEffectiveStat(Attribute.EXPERTISEPERCENT),
		this.getEffectiveStat(Attribute.EXPERTISERATING));
	output += String.format("HIT: %s (%s)\n", this.getEffectiveStat(Attribute.HITPERCENT),
		this.getEffectiveStat(Attribute.HITRATING));
	output += String.format("HASTE: %s (%s)\n", this.getEffectiveStat(Attribute.HASTEPERCENT),
		this.getEffectiveStat(Attribute.HASTERATING));

	// tradeskills
	output += "Tradeskills: [\n";
	for( String s: this.tradeskills.keySet() ) {
	    Integer value = this.tradeskills.get(s);

	    // format and show tradeskill
	    output += String.format("\t%s at level %s\n", s.toUpperCase(), value.toString());
	}
	output += "]\n";

	// equipment
	output += "Equipment: [\n";

	for( Equipment e: this.equipmentTable.values() ) {
	    output += e.toString() + "\n";
	}

	output += "]";

	return output;
    }

    /**
     * Recalc all stats.
     * 
     */
    public void recalc() {

	// calculated effective stats from equipment
	this.calculateEffectiveStatsFromEquipment();

    }

    /**
     * Clear all the talents.
     */
    public void clearTalents() {
	this.talents.clear();
    }

    /**
     * Add a talent for the player to use
     * 
     * @param t
     */
    public void addTalent( Talent t ) {

	// bail on bad talent
	if( t == null || t.getName() == null || t.getName().isEmpty() )
	    return;

	// bail on talent for wrong class
	if( !t.forClass().equals(this.getClassType()) )
	    return;

	// remove talents
	for( Talent i: this.talents ) {
	    if( i.getName().equals(t.getName()) && i.getLevel() != t.getLevel() ) {
		this.talents.remove(i);
	    }
	}

	// add talent
	this.talents.add(t);
    }

    /**
     * Remove a talent instance
     * 
     * @param t
     */
    public void removeTalent( Talent t ) {

	this.talents.remove(t);

    }

    /**
     * Remove a talent by name. Removes spaces, and makes lower case to make
     * comparison as foolproof as possible.
     * 
     * @param name
     */
    public void removeTalent( String name ) {

	// don't remove if the name is blank or empty
	if( name == null || name.isEmpty() )
	    return;

	// aggressive
	String tname = name.toLowerCase().trim().replace(" ", "").replace("_", "");

	int i = 0;
	while( i > this.talents.size() ) {
	    // get name of current talent
	    Talent t = this.talents.get(i);
	    String xname = t.getName().toLowerCase().trim().replace(" ", "").replace("_", "");

	    // if the talent names match
	    if( xname.equals(tname) ) {
		// remove from index
		this.talents.remove(i);
	    } else {
		// increment because the item wasn't removed
		// and that is the only way to get to the next
		// ;item
		i++;
	    }

	}

    }

    /**
     * Get the talent from the player with the given name.
     * 
     * @param name
     * @return
     * @throws IllegalStateException when the input name is null or empty
     * @throws TalentNotFoundException
     */
    public Talent getTalent( String name ) throws IllegalArgumentException, TalentNotFoundException {

	// don't remove if the name is blank or empty
	if( name == null || name.isEmpty() )
	    throw new IllegalArgumentException("The talent name must not be null or empty.");

	// aggressive
	String tname = name.toLowerCase().trim().replace(" ", "").replace("_", "");

	// talent to return
	Talent t = null;

	// simple search through talents
	for( Talent x: this.talents ) {
	    // get the name
	    String xname = x.getName().toLowerCase().trim().replace(" ", "").replace("_", "");

	    // if the names are equal
	    if( tname.equals(xname) ) {
		t = x;
		break;
	    }
	}

	if( t == null ) {
	    throw new TalentNotFoundException("The talent \"" + name + "\" could not be found");
	}

	return t;
    }

    /**
     * List of active talents
     * 
     * @return
     */
    public ArrayList<Talent> getTalents() {
	return this.talents;
    }

    /**
     * Talent Factory
     * 
     * @return
     */
    protected TalentFactory getTalentFactory() {
	return new TalentFactory();
    }

    /**
     * Use the talent factory to create the talent
     * 
     * @param talentName
     * @param level
     */
    public void addTalent( String talentName, int level ) {
	this.addTalent(this.getTalentFactory().createTalent(talentName, level, this.getClassType()));
    }

    /**
     * Add all of the talents in a given list
     * 
     * @param talents
     */
    public void addTalents( Collection<Talent> talents ) {
	this.talents.addAll(talents);
    }

    /**
     * Returns true if the player has the given tradeskill. Returns false if the
     * player does not.
     * 
     * @param skillName
     * @return
     */
    public boolean hasTradeSkill( String skillName ) {
	boolean answer = this.tradeskills.containsKey(skillName.toUpperCase());
	return answer;
    }

    /**
     * An arraylist of strings that contains the names of all the tradeskills
     * available to the player.
     * 
     * @return
     */
    public ArrayList<String> getTradeSkills() {
	ArrayList<String> skillList = new ArrayList<String>();
	skillList.addAll(this.tradeskills.keySet());
	return skillList;
    }

    /**
     * Get the value for a given tradeskill. Returns 0 if the player does not
     * have that tradeskill.
     * 
     * @param skillName
     * @return
     */
    public Integer getTradeSkillValue( String skillName ) {
	Integer newInt = new Integer(0);

	// return
	if( this.tradeskills.containsKey(skillName.toUpperCase()) ) {
	    newInt = this.tradeskills.get(skillName.toUpperCase());
	}

	return newInt;
    }

    /**
     * Adds a skillvalue to the skillvalue table
     * 
     * @param skillName
     * @param skilLValue
     */
    public void setTradeSkillValue( String skillName, Integer skillValue ) {
	if( skillValue == null || skillValue.intValue() == 0 )
	    return;

	String tradeSkillName = skillName.toUpperCase();
	if( this.tradeskills.containsKey(tradeSkillName) ) {
	    this.tradeskills.remove(tradeSkillName);
	}
	this.tradeskills.put(tradeSkillName, skillValue);
    }

    /**
     * Return the equipment item that is equiped in the given slot. Will return
     * null if no equipment is equiped in the given slot.
     * 
     * @param equipedSlot
     * @return
     */
    public Equipment getEquipedItem( Slot equipedSlot ) {

	Equipment e = null;

	if( this.equipmentTable.containsKey(equipedSlot) ) {
	    e = this.equipmentTable.get(equipedSlot);
	}

	return e;
    }

    /**
     * Get the slots equiped on the player. Returns an empty set when no slots
     * are equiped.
     * 
     * @return
     */
    public Set<Slot> getEquipedSlots() {
	Set<Slot> equipedSlots = new HashSet<Slot>();
	equipedSlots.addAll(this.equipmentTable.keySet());
	return equipedSlots;
    }
}
